import gzip
import os
from multiprocessing import freeze_support

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torch.utils.data import Dataset

from main_pytorch_02_model_define import SimpleCNN


# 定义自定义数据集类
class LocalMNIST(Dataset):
    def __init__(self, root_dir, train=True, transform=None):
        self.root_dir = root_dir
        self.transform = transform
        self.train = train

        # 加载图像和标签
        if self.train:
            self.images_path = os.path.join(root_dir, 'train-images-idx3-ubyte.gz')
            self.labels_path = os.path.join(root_dir, 'train-labels-idx1-ubyte.gz')
        else:
            self.images_path = os.path.join(root_dir, 't10k-images-idx3-ubyte.gz')
            self.labels_path = os.path.join(root_dir, 't10k-labels-idx1-ubyte.gz')

        try:
            self.images = self._load_images()
            self.labels = self._load_labels()
        except Exception as e:
            print(f"Error loading data: {e}")
            raise

    def _load_images(self):
        with gzip.open(self.images_path, 'rb') as f:
            images = np.frombuffer(f.read(), np.uint8, offset=16).reshape(-1, 28, 28)
        return images

    def _load_labels(self):
        with gzip.open(self.labels_path, 'rb') as f:
            labels = np.frombuffer(f.read(), np.uint8, offset=8)
        return labels

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        image = self.images[idx]
        label = self.labels[idx]

        if self.transform:
            image = self.transform(image)
        return image, label


# 定义训练函数
def train(model, device, train_loader, optimizer, criterion, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

        if batch_idx % 100 == 0:
            print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} '
                  f'({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')


def test(model, device, test_loader, criterion):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += criterion(output, target).item()  # sum up batch loss
            pred = output.argmax(dim=1, keepdim=True)  # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)

    print(f'\nTest set: Average loss: {test_loss:.4f}, Accuracy: {correct}/{len(test_loader.dataset)} '
          f'({100. * correct / len(test_loader.dataset):.0f}%)\n')


if __name__ == '__main__':
    freeze_support()
    # 加载数据集并进行训练
    # 定义MNIST文件对应的路径
    MNIST_FILE_PATH = 'D:/TT_WORK+/PyCharm/20250109_1_CNN/MNIST/'

    # 设置设备
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f'Current device is {device}')

    # 定义数据预处理
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])

    # 创建训练集和测试集
    batch_size = 64
    num_workers = 2

    train_dataset = LocalMNIST(root_dir=MNIST_FILE_PATH, train=True, transform=transform)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)

    test_dataset = LocalMNIST(root_dir=MNIST_FILE_PATH, train=False, transform=transform)
    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=num_workers)

    # 创建模型、损失函数和优化器
    model = SimpleCNN().to(device)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    # 训练模型
    epochs = 5
    for epoch in range(1, epochs + 1):
        train(model, device, train_loader, optimizer, criterion, epoch)
        test(model, device, test_loader, criterion)

        # 保存模型
        torch.save(model.state_dict(), f'model_epoch_{epoch}.pth')
        print(f'Model saved at epoch {epoch}')

    # 保存完整模型
    torch.save(model, f'full_model2.pth')
    print(f'Full Model saved successfully')
